﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: IdentityMap.cs
//
//  Description: This class represents an identity map for persistents.
// 
//--------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;
using Chili.Opf3.Mapping;
using Chili.Opf3.Relations;

namespace Chili.Opf3
{
	/// <summary>
	/// This class represents an identity map for objects that are currently in memory.
	/// </summary>
	/// <remarks>
	/// An identity map is a class that holds the persistent objects that are currently in memory. 
	/// During a load (with enabled identity map) all objects that are loaded from the storage are compared
	/// against the one in the map. If an object is already in the map that one will be returned instead
	/// of the one that came from the storage.
	/// </remarks>
	public sealed class IdentityMap
	{
		private WeakReferencesCache<string, object> _instances = new WeakReferencesCache<string, object>();
		private bool _enabled;

		/// <summary>
		/// Adds a persistent object to the identity map.
		/// </summary>
		/// <param name="obj">The persistent object that is added to the map.</param>
		public void Add(object obj)
		{
			if (obj == null)
				throw new ArgumentNullException("obj");

			// return if not enabled.
			if (!this.Enabled)
				return;

			// only persistent objects can be added to the identity map.
			if (!TypeMapping.HasPersistentMapping(obj.GetType()))
				throw new InvalidOperationException("Can't add object that is not a persistent object.");

			// if no identity columns have been found just return. We can't add this item
			// to the map because there's no way to generate a key.
			TypeMapping mapping = TypeMapping.GetTypeMapping(obj);
			if (mapping.Members.GetIdentifierMemberMappings().Count == 0)
				return;

			string key = BuildUniqueKey(obj, mapping);
			// only add the object when not yet found in the cache.
			if (!_instances.ContainsKey(key))
				_instances.Add(key, obj, null);
		}

		/// <summary>
		/// Tries to get the object from the identity map. If the object couldn't be found
		/// the result is false.
		/// </summary>
		/// <param name="obj">The object that's searched in the identity map. Only the identity properties need
		/// to be set to make this work properly.</param>
		/// <param name="result">The result that represents the object in the identity map.</param>
		/// <returns>True if the object has been found; otherwise false.</returns>
		public bool TryGetValue<T>(T obj, out T result)
		{
			result = default(T);

			// return if not enabled.
			if (!this.Enabled)
				return false;

			// if no identity columns have been found just return. We can't add this item
			// to the map because there's no way to generate a key.
			TypeMapping mapping = TypeMapping.GetTypeMapping(obj);
			if (mapping.Members.GetIdentifierMemberMappings().Count == 0)
				return false;

			string key = BuildUniqueKey(obj, mapping);
			result = (T)_instances.GetFirstObjectByKey(key);

			return (result != null);
		}

		/// <summary>
		/// Builds a valid unique key for the object.
		/// </summary>
		private string BuildUniqueKey(object obj, TypeMapping mapping)
		{
			// create a string builder to create the identifier key part.
			StringBuilder builder = new StringBuilder();

			// create the key part of the item.
			IIdentifierMemberInfoCollection ids = mapping.Members.GetIdentifierMemberMappings();
			foreach (IMemberInfo member in ids)
			{
				builder.Append(member.FieldAttribute.FieldName);
				builder.AppendFormat("({0}): {1} ", member.FieldAttribute.FieldName.Length, member.GetValue(obj));
			}

			// return the key that contains also the entity where the item was fetched from.
			return string.Format("{0}@ {1}", builder.ToString(), mapping.PersistentType.AssemblyQualifiedName);
		}

		/// <summary>
		/// Gets or sets whether the <see cref="IdentityMap" /> is enabled.
		/// </summary>
		public bool Enabled
		{
			get { return _enabled; }
			set { _enabled = value; }
		}
		
		/// <summary>
		/// Removes the given object from the <see cref="IdentityMap" />.
		/// </summary>
		/// <param name="obj">The object that is removed from the <see cref="IdentityMap" />.</param>
		public void Remove(object obj)
		{
			Remove(obj, false);
		}

		/// <summary>
		/// Removes the given object from the <see cref="IdentityMap" />. If specified also all
		/// related and loaded objects are removed from the map.
		/// </summary>
		/// <param name="obj">The object that is removed from the <see cref="IdentityMap" />.</param>
		/// <param name="fullLoadedObjectGraph">If true all related and loaded objects are removed too.</param>
		public void Remove(object obj, bool fullLoadedObjectGraph)
		{
			Remove(obj, fullLoadedObjectGraph, new Dictionary<object, object>());
		}

		/// <summary>
		/// Removes the given object from the <see cref="IdentityMap" />. If specified also all
		/// related and loaded objects are removed from the map.
		/// </summary>
		/// <param name="obj">The object that is removed from the <see cref="IdentityMap" />.</param>
		/// <param name="fullLoadedObjectGraph">If true all related and loaded objects are removed too.</param>
		/// <param name="trace">The trace that is used to avoid circles.</param>
		private void Remove(object obj, bool fullLoadedObjectGraph, Dictionary<object, object> trace)
		{
			if (obj == null)
				throw new ArgumentNullException("obj");

			_instances.Remove(BuildUniqueKey(obj, TypeMapping.GetTypeMapping(obj)));
			// add the current object to the trace. This signalizes that the object has been
			// removed from the identity map.
			trace.Add(obj, obj);

			// return if only a flat remove is done.
			if (!fullLoadedObjectGraph)
				return;

			// get the type mapping for the object.
			TypeMapping mapping = TypeMapping.GetTypeMapping(obj);
			
			// get all relations from the mapping.
			foreach (IRelationInfo relation in mapping.Relations)
			{
				// get the object holder instance from the relation.
				ObjectHolderBase holder = relation.GetObjectHolder(obj);
				if (holder == null)
					continue;

				// loop through all objects in the object holder ...
				foreach (object relObj in holder.GetRawEnumerator())
				{
					// skip the object if already traced.
					if (trace.ContainsKey(relObj))
						continue;
					
					// ... and do a recursive remove.
					this.Remove(relObj, true, trace);
				}
			}
		}
		
		/// <summary>
		/// Removes all objects from the <see cref="IdentityMap" />.
		/// </summary>
		public void RemoveAll()
		{
			_instances.RemoveAll();
		}
	}
}
